package de.eridea.learning.java.usermanagement.logic;

import java.util.HashSet;
import java.util.Set;

import org.springframework.transaction.annotation.Transactional;
import de.eridea.learning.java.usermanagement.logic.exception.InsufficientDataException;
import de.eridea.learning.java.usermanagement.logic.exception.MalformedDataException;
import de.eridea.learning.java.usermanagement.logic.exception.NameAlreadyExistsException;
import de.eridea.learning.java.usermanagement.logic.exception.NoSuchGroupException;
import de.eridea.learning.java.usermanagement.logic.exception.NoSuchResourceException;
import de.eridea.learning.java.usermanagement.logic.exception.NoSuchUserException;
import de.eridea.learning.java.usermanagement.logic.exception.NothingToDoException;
import de.eridea.learning.java.usermanagement.logic.exception.VerificationFailedException;
import de.eridea.learning.java.usermanagement.logic.to.GroupData;
import de.eridea.learning.java.usermanagement.logic.to.GroupDataEditable;
import de.eridea.learning.java.usermanagement.logic.to.GroupDataImpl;
import de.eridea.learning.java.usermanagement.logic.to.GroupDataLinked;
import de.eridea.learning.java.usermanagement.logic.to.ResourceData;
import de.eridea.learning.java.usermanagement.logic.to.ResourceDataEditable;
import de.eridea.learning.java.usermanagement.logic.to.ResourceDataImpl;
import de.eridea.learning.java.usermanagement.logic.to.ResourceDataLinked;
import de.eridea.learning.java.usermanagement.logic.to.UserData;
import de.eridea.learning.java.usermanagement.logic.to.UserDataEditable;
import de.eridea.learning.java.usermanagement.logic.to.UserDataImpl;
import de.eridea.learning.java.usermanagement.logic.to.UserDataLinked;
import de.eridea.learning.java.usermanagement.persistence.Group;
import de.eridea.learning.java.usermanagement.persistence.Resource;
import de.eridea.learning.java.usermanagement.persistence.User;
import de.eridea.learning.java.usermanagement.persistence.UserManager;

public class UserServiceImpl implements UserService {

	private UserManager userManager = null;

	@Transactional
	public UserDataLinked login(String loginName, String password)
			throws VerificationFailedException, NoSuchUserException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		User user = userManager.getUser(loginName);
		if (user == null) {
			throw new NoSuchUserException(loginName);
		}
		// TODO use actual hash functions
		if (!user.getPasswordHash().equals(password)) {
			throw new VerificationFailedException(
					"Incorrect password for user " + loginName);
		}

		return userToUserData(user);
	}

	@Transactional
	public UserDataLinked getUser(String loginName) throws NoSuchUserException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		User user = userManager.getUser(loginName);
		if (user == null) {
			throw new NoSuchUserException(loginName);
		}
		return userToUserData(user);
	}

	public UserDataEditable newUser(String loginName) {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		return new UserDataImpl(loginName, new HashSet<GroupData>(),
				new HashSet<GroupData>(), new HashSet<ResourceData>());
	}

	@Transactional
	public UserDataLinked registerUser(UserDataEditable userData,
			String password) throws InsufficientDataException,
			MalformedDataException, NameAlreadyExistsException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		Set<String> missingProperties = new HashSet<String>();
		if (userData.getLoginName() == null) {
			missingProperties.add("loginName");
		}
		if (!missingProperties.isEmpty()) {
			throw new InsufficientDataException(missingProperties);
		}
		UserDataLinked result = null;
		if (userManager.nameExists(User.class, "loginName", userData.getLoginName())) {
			throw new NameAlreadyExistsException();
		}
		if (userManager.nameExists(User.class, "displayName", userData.getDisplayName())) {
			throw new NameAlreadyExistsException();
		}
		User user = userManager.createUserObject();
		user.setLoginName(userData.getLoginName());
		user.setDisplayName(userData.getDisplayName());

		// TODO switch to actual hash functions
		user.setPasswordHash(password);
		userManager.persistUser(user);
		result = userToUserData(user);
		return result;
	}

	@Transactional
	public void updateUser(UserDataEditable user)
			throws InsufficientDataException, MalformedDataException,
			IllegalArgumentException, NoSuchUserException, NameAlreadyExistsException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		User persistentUser = userManager.getUser(user.getLoginName());
		if (persistentUser == null) {
			throw new NoSuchUserException(user.getLoginName());
		}
		if (userManager.nameExists(User.class, "displayName", user.getDisplayName())) {
			throw new NameAlreadyExistsException(user.getDisplayName());
		}
		persistentUser.setDisplayName(user.getDisplayName());

		// TODO copy properties (when there are mutable properties not stemming
		// from relationships)
		userManager.updateUser(persistentUser);
	}

	@Transactional
	public void updateUserPassword(String user, String newPassword)
			throws NoSuchUserException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		if (user == null) {
			throw new NullPointerException("user must not be null.");
		}
		if (newPassword == null) {
			throw new NullPointerException("newPassword must not be null.");
		}
		User persistentUser = userManager.getUser(user);
		if (persistentUser == null) {
			throw new NoSuchUserException(user);
		}
		// TODO switch to actual hash functions
		persistentUser.setPasswordHash(newPassword);
		userManager.updateUser(persistentUser);

	}

	@Transactional
	public GroupDataLinked getGroup(String groupName)
			throws NoSuchGroupException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		Group group = userManager.getGroup(groupName);
		if (group == null) {
			throw new NoSuchGroupException(groupName);
		}
		return groupToGroupData(group);
	}

	public GroupDataEditable newGroup(String groupName) {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		return new GroupDataImpl(groupName);
	}

	@Transactional
	public GroupDataLinked registerGroup(GroupDataEditable groupData)
			throws InsufficientDataException, MalformedDataException,
			NameAlreadyExistsException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		Set<String> missingProperties = new HashSet<String>();
		if (groupData.getGroupName() == null) {
			missingProperties.add("groupName");
		}
		if (groupData.getOwner() == null) {
			missingProperties.add("owner");
		}
		if (!missingProperties.isEmpty()) {
			throw new InsufficientDataException(missingProperties);
		}
		// TODO check that owner exists
		if (userManager.getUser(groupData.getGroupName()) != null) {
			throw new NameAlreadyExistsException(groupData.getGroupName());
		}
		Group group = userManager.createGroupObject();
		group.setName(groupData.getGroupName());
		group.setOwner(userManager.getUser(groupData.getOwner().getLoginName()));
		userManager.persistGroup(group);
		return groupToGroupData(group);
	}

	@Transactional
	public void updateGroup(GroupDataEditable group)
			throws InsufficientDataException, MalformedDataException,
			NoSuchGroupException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		Group persistentGroup = userManager.getGroup(group.getGroupName());
		if (persistentGroup == null) {
			throw new NoSuchGroupException(group.getGroupName());
		}
		Set<String> missingProperties = new HashSet<String>();
		if (group.getOwner() == null) {
			missingProperties.add("owner");
		}
		if (!missingProperties.isEmpty()) {
			throw new InsufficientDataException(missingProperties);
		}
		// TODO check that owner does exist
		persistentGroup.setOwner(userManager.getUser(group.getOwner()
				.getLoginName()));
		userManager.updateGroup(persistentGroup);

	}

	@Transactional
	public Set<UserData> getUsersInGroup(String groupName)
			throws NoSuchGroupException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		Group group = userManager.getGroup(groupName);
		if (group == null) {
			throw new NoSuchGroupException(groupName);
		}
		Set<UserData> users = new HashSet<UserData>();
		for (User user : group.users) {
			users.add(userToUserData(user));
		}
		return users;
	}

	@Transactional
	public ResourceDataLinked getResource(String resourceIdentifier)
			throws NoSuchResourceException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		Resource resource = userManager.getResource(resourceIdentifier);
		if (resource == null) {
			throw new NoSuchResourceException(resourceIdentifier);
		}
		return resourceToResourceData(resource);
	}

	public ResourceDataEditable newResource(String resourceIdentifier) {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		return new ResourceDataImpl(resourceIdentifier,
				new HashSet<GroupData>());
	}

	@Transactional
	public ResourceDataLinked registerResource(ResourceDataEditable resourceData)
			throws InsufficientDataException, MalformedDataException,
			NameAlreadyExistsException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		if (userManager.getResource(resourceData.getResourceIdentifier()) != null) {
			throw new NameAlreadyExistsException(
					resourceData.getResourceIdentifier());
		}
		Set<String> missingProperties = new HashSet<String>();
		if (resourceData.getResourceIdentifier() == null) {
			missingProperties.add("resourceIdentifier");
		}
		if (resourceData.getOwner() == null) {
			missingProperties.add("owner");
		}
		if (!missingProperties.isEmpty()) {
			throw new InsufficientDataException(missingProperties);
		}
		// TOTO chek that owner exists
		Resource resource = userManager.createResourceObject();
		resource.setIdString(resourceData.getResourceIdentifier());
		resource.setOwner(userManager.getUser(resourceData.getOwner()
				.getLoginName()));
		return resourceToResourceData(resource);
	}

	@Transactional
	public void updateResource(ResourceDataEditable resource)
			throws InsufficientDataException, MalformedDataException,
			IllegalArgumentException, NoSuchResourceException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		Resource persistentResource = userManager.getResource(resource
				.getResourceIdentifier());
		if (persistentResource == null) {
			throw new NoSuchResourceException(resource.getResourceIdentifier());
		}
		Set<String> missingProperties = new HashSet<String>();
		if (resource.getOwner() == null) {
			missingProperties.add("owner");
		}
		if (!missingProperties.isEmpty()) {
			throw new InsufficientDataException(missingProperties);
		}
		if (resource.getOwner().getLoginName() == null) {
			throw new IllegalArgumentException("Name of owner is null");
		}
		String ownerName = resource.getOwner().getLoginName();
		User owner = userManager.getUser(ownerName);
		if (owner == null) {
			throw new IllegalArgumentException("Unknown user as owner: "
					+ ownerName);
		}
		persistentResource.setOwner(owner);
		userManager.updateResource(persistentResource);
	}

	@Transactional
	public boolean isUserInGroup(String user, String group) {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		return userManager.isUserInGroup(user, group);
	}

	@Transactional
	public void addUserToGroup(String user, String group)
			throws NothingToDoException, NoSuchUserException,
			NoSuchGroupException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		if (!userManager.addUserToGroup(user, group)) {
			throw new NothingToDoException("User " + user
					+ " already was in Group " + group);
		}
	}

	@Transactional
	public void removeUserFromGroup(String user, String group)
			throws NothingToDoException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		if (!userManager.removeUserFromGroup(user, group)) {
			throw new NothingToDoException("User " + user
					+ " was not in Group " + group);
		}
	}

	@Transactional
	public boolean hasPermission(String user, String resource) {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		return userManager.isUserInPermittedGroupForResource(user, resource);
	}

	@Transactional
	public void setUserManager(UserManager userManager) {
		if (this.userManager != null) {
			throw new IllegalStateException("userManager was set already.");
		}
		if (userManager == null) {
			throw new NullPointerException("userManager must not be null.");
		}
		this.userManager = userManager;
	}

	@Transactional
	private UserDataImpl userToUserDataUnlinked(User user) {
		UserDataImpl result = new UserDataImpl(user.getLoginName(),
				new HashSet<GroupData>(), new HashSet<GroupData>(),
				new HashSet<ResourceData>());
		result.setDisplayName(user.getDisplayName());
		return result;
	}

	@Transactional
	private UserDataImpl userToUserData(User user) {
		UserDataImpl result = userToUserDataUnlinked(user);
		GroupDataImpl groupData;
		Set<GroupData> groups = result.getGroups();
		for (Group group : user.getGroups()) {
			groupData = groupToGroupDataUnlinked(group);
			groups.add(groupData);
		}
		groups = result.getOwnedGroups();
		for (Group group : user.getOwnedGroups()) {
			groupData = groupToGroupDataUnlinked(group);
			groupData.setOwner(result);
			groups.add(groupData);
		}
		ResourceDataImpl resourceData;
		Set<ResourceData> resources = result.getOwnedResources();
		for (Resource resource : user.getOwnedResources()) {
			resourceData = resourceToResourceDataUnlinked(resource);
			resourceData.setOwner(result);
			resources.add(resourceData);
		}
		return result;
	}

	private ResourceDataImpl resourceToResourceDataUnlinked(Resource resource) {
		ResourceDataImpl result = new ResourceDataImpl(resource.getIdString(),
				new HashSet<GroupData>());
		UserDataImpl owner = userToUserDataUnlinked(resource.getOwner());
		result.setOwner(owner);
		return result;
	}

	private ResourceDataImpl resourceToResourceData(Resource resource) {
		ResourceDataImpl result = resourceToResourceDataUnlinked(resource);
		GroupDataImpl groupData;
		Set<GroupData> groups = result.getPermittedGroups();
		for (Group group : resource.groups) {
			groupData = groupToGroupDataUnlinked(group);
			groups.add((GroupData) groupData);
		}
		return result;
	}

	private GroupDataImpl groupToGroupDataUnlinked(Group group) {
		GroupDataImpl result = new GroupDataImpl(group.getName());
		result.setOwner(userToUserDataUnlinked(group.getOwner()));
		return result;
	}

	private GroupDataImpl groupToGroupData(Group group) {
		GroupDataImpl result = groupToGroupDataUnlinked(group);
		return result;
	}

	@Transactional
	public UserDataEditable getEditableUser(String loginName)
			throws NoSuchUserException {
		return (UserDataEditable) getUser(loginName);
	}

	@Transactional
	public GroupDataEditable getEditableGroup(String groupName)
			throws NoSuchGroupException {
		return (GroupDataEditable) getGroup(groupName);
	}

	@Transactional
	public ResourceDataEditable getEditableResource(String resourceIdentifier)
			throws NoSuchResourceException {
		return (ResourceDataEditable) getResource(resourceIdentifier);
	}

	@Transactional
	public void grantPermission(String group, String resource)
			throws NothingToDoException, NoSuchGroupException,
			NoSuchResourceException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		if (!userManager.grantPermission(group, resource)) {
			throw new NothingToDoException("Permission already was granted");
		}
	}

	@Transactional
	public void revokePermission(String group, String resource)
			throws NothingToDoException, NoSuchGroupException,
			NoSuchResourceException {
		if (userManager == null) {
			throw new IllegalStateException(
					"userManager must be set before this UserServiceImpl can be used.");
		}
		if (!userManager.revokePermission(group, resource)) {
			throw new NothingToDoException(
					"Permission has not been granted before");
		}
	}

	@Transactional
	public Set<GroupData> getGroups() {
		HashSet<GroupData> result = new HashSet<GroupData>();
		Set<Group> groups = userManager.getGroups();
		for(Group group : groups) {
			result.add(groupToGroupDataUnlinked(group));
		}
		return result;
	}

}
